
DEBUG = False

from amplitude.models import Model
import amplitude.lib.id
import hashlib, re, sys

class User(Model):
    """usage:
    >>> # Connect to the amplitude test db
    >>> import amplitude; db=amplitude.config.connect_to_db()
    >>> # We need a users table in our test db --
    >>> # the minimal users table is email, pwd, varchar, registered, verified. 
    >>> db.execute("create table users (email varchar primary key, pwd varchar, salt varchar, registered timestamp default now(), verified timestamp)")
    >>> 
    >>> user = User(db, email='nobody@home.now')        # valid in form if not in destination
    >>> user.set_password('insecure')                   # len passowrd >= User.MIN_PASSWORD_LEN
    True
    >>> user.insert()                                   # should be fine, now the user is registered
    >>> user.registered is not None
    True
    >>> 
    >>> # If you try to insert a user with an invalid email address or password, it doesn't work.
    >>> anotheruser = User(db, email='nobody@home')     # not valid
    >>> anotheruser.set_password('short')               # too short -- not set
    False
    >>> anotheruser.insert()
    Traceback (most recent call last):
        ...
    ValueError: email is not valid: nobody@home
    >>> 
    >>> # You can also give the user a random password, but be sure to tell them what it is
    >>> pwd = User.random_password()                    # return this to the user somehow
    >>> user.set_password(pwd)
    True
    >>> user.update()
    >>> 
    >>> # Now we'll try to authenticate our valid first user
    >>> authuser = User(db).authenticate('nobody@home.now', pwd)    # now we use the random pwd
    >>> authuser is not None
    False
    >>> # but it didn't work (no result was returned) because the user isn't verified.
    >>> user.verified == None
    True
    >>> # We could override the need for verification and just let people log in who haven't verified their email address
    >>> authuser = User(db).authenticate('nobody@home.now', pwd, unverified=True)
    >>> authuser is not None
    True
    >>> # But it's best to make users click the emailed link.
    >>> # So let's suppose our user has clicked the "verify" link in the email we sent them
    >>> user.verify_now()                               # method that sets the user's "verified" attribute to now.
    >>> # Now we can authenticate normally
    >>> authuser = User(db).authenticate('nobody@home.now', pwd)
    >>> authuser is not None
    True
    >>> authuser.email == user.email
    True
    >>> 
    >>> # clean up test db
    >>> db.execute("drop table users")
    >>> 
    >>> # So that's how you work with user accounts.
    """

    relation='users'
    pk_fields=['email']
    
    MIN_PASSWORD_LEN = 6
    PASSWORD_CHARSET = amplitude.lib.id.hex_chars
    
    def get_id(self):
        return self.email
        
    def register(self):
        """try to register the given user, returning any errors that occur."""
        errors = []
        errors = self.__class__.validate_email(self.email, errors)        
        errors = self.__class__.validate_password(self.password, errors)
        
        if type(self.password) in [str, unicode]:
            self.password = self.password.strip()
            if self.password != '': 
                autogen = False     # the password was not autogenerated
            else:
                self.password = self.__class__.random_password()
                autogen = True      # the password _was_ autogenerated

        if errors != []:
            return errors
        else:
            try:
                self.set_password(self.password)
                password = self.password         # temp storage
                del self['password']             # remove password from model object for insert
                self.insert()
                if autogen == True:
                    self.password = password     # put password back in if autogenerated (for mailers to use.)
            except:
                if 'IntegrityError' in str(sys.exc_info()[0]):
                    errors = ["That email address is already registered."]                    
                else:
                    errors = [sys.exc_info()[1]]
                print sys.exc_info()
                return errors
        # if no errors, returns None

    # override base class insert() and update() to ensure that email is valid
    def insert(self, **args):
        if not self.__class__.email_is_valid(self.email):
            raise ValueError, "email is not valid: %s" % self.email
        Model.insert(self, **args)
        
    def update(self, **args):
        if not self.__class__.email_is_valid(self.email):
            raise ValueError, "email is not valid: %s" % self.email
        Model.update(self, **args)
    
    def verify(self, id, key):
        """verify the given id & key, returning any errors that occur."""
        user = self.select_one(email=id, salt=key)
        if user is not None:
            user.verify_now()
            return None
        else:
            return "Not verified"

    def verify_now(self):
        self.db.execute("update %s set verified=now() where email=%s" % (self.__class__.relation, self.quote(self.email)))
        self = self.reload()

    # -- authentication stuff --

    def set_password(self, passwd, errors=[]):
        """sets the user's password."""
        if self.__class__.validate_password(passwd, errors=errors)==[]:
            self.salt = self.make_salt()
            self.pwd = self.encrypt_password(passwd, self.salt)
            return True
        else:
            return False

    def authenticate(self, email, password, unverified=False):
        """Authenticate the login against the database."""
        user = self.select_one(where="email ~* %s" % self.quote(email)) # case-insensitive and secure
        if DEBUG==True: 
            print "User.authenticate('%s', '%s')" % (email, password)
            print "  user:", user and user.email
            print "  pwd :", user.pwd
            print "  crpt:", user.encrypt_password(password, user.salt)
            print "  ver.:", user.verified or unverified
        if (user is not None
            and user.pwd == user.encrypt_password(password, user.salt)
            and (user.verified or unverified)):
                if DEBUG==True: print " ==> authentication successful"
                return user

    # -- password stuff -- 

    @classmethod
    def random_password(cls, length=None, charset=None):
        if length==None: length = cls.MIN_PASSWORD_LEN
        if charset==None: charset = cls.PASSWORD_CHARSET
        return amplitude.lib.id.random_id(length=length, charset=charset)

    @classmethod
    def make_salt(cls, salt_key=''):
        h = hashlib.sha256()
        h.update('we have our secrets, too')
        if salt_key != '': h.update(salt_key)
        h.update(amplitude.lib.id.random_id(79, amplitude.lib.id.ascii_chars))
        return h.hexdigest()

    @classmethod
    def encrypt_password(cls, password, salt):
        h = hashlib.sha256()
        h.update(password)
        h.update(salt)
        return h.hexdigest()

    # -- validations --

    @classmethod
    def email_is_valid(cls, email):
        if validate_email(email) == []:
            return True
        else:
            return False
    
    @classmethod
    def validate_email(cls, email, errors=[]):
        if type(email) in [str, unicode]: 
            email = email.strip()
        else:
            errors.append("Email must be text.")
        if email in [None, '']:
            errors.append("Please type your email address.")
        elif not cls.email_is_valid(email):
            errors.append("Email must be a valid email address.")
        return errors
        
    @classmethod
    def validate_password(cls, password, errors=[]):
        if not cls.password_length_valid(password):
            errors.append(
                "Password must be at least %d characters long (or leave it blank to generate one automatically)." 
                % cls.MIN_PASSWORD_LEN)
        return errors
    
    @classmethod
    def email_is_valid(cls, email):
        email_regexp = "^[\w\.%\-=]+@(?:[a-z0-9-]+\.)+([a-z]{2,4}|museum)$"  # case-insensitive
        return re.match(email_regexp, email, re.I + re.U) and True or False

    @classmethod
    def email_filtered(cls, email):
        """returns a verified clean version of the email address, might not be a real address"""
        pat = "[^\w\.\-%=@]"
        return re.sub(pat, ".", email, re.U+re.I).strip()
        
    @classmethod
    def password_length_valid(cls, passwd):
        if type(passwd) not in (str, unicode):
            return False
        if len(passwd.strip()) < cls.MIN_PASSWORD_LEN:
            return False    
        return True
    

if __name__ == '__main__':
    import doctest
    doctest.testmod()
